package com.dongwukj.weiwei.ui.widget;

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.KeyEventCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Interpolator;
import android.widget.Scroller;

/**
 * Layout manager that allows the user to flip left and right through pages of
 * data. You supply an implementation of a
 * {@link android.support.v4.view.PagerAdapter} to generate the pages that the
 * view shows.
 * 
 * <p>
 * Note this class is currently under early design and development. The API will
 * likely change in later updates of the compatibility library, requiring
 * changes to the source code of apps when they are compiled against the newer
 * version.
 * </p>
 */
public class LazyViewPager extends ViewGroup {
  private static final String TAG = "LazyViewPager";
  private static final boolean DEBUG = false;

  private static final boolean USE_CACHE = false;

  private static final int DEFAULT_OFFSCREEN_PAGES = 0;// 默认的加载页面,ViewPager是1个,所以会加载两个Fragment
  private static final int MAX_SETTLE_DURATION = 600; // ms

  static class ItemInfo {
    Object object;
    int position;
    boolean scrolling;
  }

  private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() {
    @Override
    public int compare(ItemInfo lhs, ItemInfo rhs) {
      return lhs.position - rhs.position;
    }
  };

  private static final Interpolator sInterpolator = new Interpolator() {
    public float getInterpolation(float t) {
      // _o(t) = t * t * ((tension + 1) * t + tension)
      // o(t) = _o(t - 1) + 1
      t -= 1.0f;
      return t * t * t + 1.0f;
    }
  };

  private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();

  private PagerAdapter mAdapter;
  private int mCurItem; // Index of currently displayed page.
  private int mRestoredCurItem = -1;
  private Parcelable mRestoredAdapterState = null;
  private ClassLoader mRestoredClassLoader = null;
  private Scroller mScroller;
  private PagerObserver mObserver;

  private int mPageMargin;
  private Drawable mMarginDrawable;

  private int mChildWidthMeasureSpec;
  private int mChildHeightMeasureSpec;
  private boolean mInLayout;

  private boolean mScrollingCacheEnabled;

  private boolean mPopulatePending;
  private boolean mScrolling;
  private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

  private boolean mIsBeingDragged;
  private boolean mIsUnableToDrag;
  private int mTouchSlop;
  private float mInitialMotionX;
  /**
   * Position of the last motion event.
   */
  private float mLastMotionX;
  private float mLastMotionY;
  /**
   * ID of the active pointer. This is used to retain consistency during
   * drags/flings if multiple pointers are used.
   */
  private int mActivePointerId = INVALID_POINTER;
  /**
   * Sentinel value for no current active pointer. Used by
   * {@link #mActivePointerId}.
   */
  private static final int INVALID_POINTER = -1;

  /**
   * Determines speed during touch scrolling
   */
  private VelocityTracker mVelocityTracker;
  private int mMinimumVelocity;
  private int mMaximumVelocity;
  private float mBaseLineFlingVelocity;
  private float mFlingVelocityInfluence;

  private boolean mFakeDragging;
  private long mFakeDragBeginTime;

  private EdgeEffectCompat mLeftEdge;
  private EdgeEffectCompat mRightEdge;

  private boolean mFirstLayout = true;

  private OnPageChangeListener mOnPageChangeListener;

  /**
   * Indicates that the pager is in an idle, settled state. The current page
   * is fully in view and no animation is in progress.
   */
  public static final int SCROLL_STATE_IDLE = 0;

  /**
   * Indicates that the pager is currently being dragged by the user.
   */
  public static final int SCROLL_STATE_DRAGGING = 1;

  /**
   * Indicates that the pager is in the process of settling to a final
   * position.
   */
  public static final int SCROLL_STATE_SETTLING = 2;

  private int mScrollState = SCROLL_STATE_IDLE;

  /**
   * Callback interface for responding to changing state of the selected page.
   */
  public interface OnPageChangeListener {

    /**
     * This method will be invoked when the current page is scrolled, either
     * as part of a programmatically initiated smooth scroll or a user
     * initiated touch scroll.
     * 
     * @param position
     *            Position index of the first page currently being
     *            displayed. Page position+1 will be visible if
     *            positionOffset is nonzero.
     * @param positionOffset
     *            Value from [0, 1) indicating the offset from the page at
     *            position.
     * @param positionOffsetPixels
     *            Value in pixels indicating the offset from position.
     */
    public void onPageScrolled(int position, float positionOffset,
        int positionOffsetPixels);

    /**
     * This method will be invoked when a new page becomes selected.
     * Animation is not necessarily complete.
     * 
     * @param position
     *            Position index of the new selected page.
     */
    public void onPageSelected(int position);

    /**
     * Called when the scroll state changes. Useful for discovering when the
     * user begins dragging, when the pager is automatically settling to the
     * current page, or when it is fully stopped/idle.
     * 
     * @param state
     *            The new scroll state.
     * @see android.support.v4.view.ViewPager#SCROLL_STATE_IDLE
     * @see android.support.v4.view.ViewPager#SCROLL_STATE_DRAGGING
     * @see android.support.v4.view.ViewPager#SCROLL_STATE_SETTLING
     */
    public void onPageScrollStateChanged(int state);
  }

  /**
   * Simple implementation of the
   * {@link com.bxg.news.view.LazyViewPager.OnPageChangeListener}
   * interface with stub implementations of each method. Extend this if you do
   * not intend to override every method of
   * {@link com.bxg.news.view.LazyViewPager.OnPageChangeListener}.
   */
  public static class SimpleOnPageChangeListener implements
      OnPageChangeListener {
    @Override
    public void onPageScrolled(int position, float positionOffset,
        int positionOffsetPixels) {
      // This space for rent
    }

    @Override
    public void onPageSelected(int position) {
      // This space for rent
    }

    @Override
    public void onPageScrollStateChanged(int state) {
      // This space for rent
    }
  }

  public LazyViewPager(Context context) {
    super(context);
    initViewPager();
  }

  public LazyViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    initViewPager();
  }

  void initViewPager() {
    setWillNotDraw(false);
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    setFocusable(true);
    final Context context = getContext();
    mScroller = new Scroller(context, sInterpolator);
    final ViewConfiguration configuration = ViewConfiguration.get(context);
    mTouchSlop = ViewConfigurationCompat
        .getScaledPagingTouchSlop(configuration);
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    mLeftEdge = new EdgeEffectCompat(context);
    mRightEdge = new EdgeEffectCompat(context);

    float density = context.getResources().getDisplayMetrics().density;
    mBaseLineFlingVelocity = 2500.0f * density;
    mFlingVelocityInfluence = 0.4f;
  }

  private void setScrollState(int newState) {
    if (mScrollState == newState) {
      return;
    }

    mScrollState = newState;
    if (mOnPageChangeListener != null) {
      mOnPageChangeListener.onPageScrollStateChanged(newState);
    }
  }

  public void setAdapter(PagerAdapter adapter) {
    if (mAdapter != null) {
      mAdapter.unregisterDataSetObserver(mObserver);
      mAdapter.startUpdate(this);
      for (int i = 0; i < mItems.size(); i++) {
        final ItemInfo ii = mItems.get(i);
        mAdapter.destroyItem(this, ii.position, ii.object);
      }
      mAdapter.finishUpdate(this);
      mItems.clear();
      removeAllViews();
      mCurItem = 0;
      scrollTo(0, 0);
    }

    mAdapter = adapter;

    if (mAdapter != null) {
      if (mObserver == null) {
        mObserver = new PagerObserver();
      }
      mAdapter.registerDataSetObserver(mObserver);
      mPopulatePending = false;
      if (mRestoredCurItem >= 0) {
        mAdapter.restoreState(mRestoredAdapterState,
            mRestoredClassLoader);
        setCurrentItemInternal(mRestoredCurItem, false, true);
        mRestoredCurItem = -1;
        mRestoredAdapterState = null;
        mRestoredClassLoader = null;
      } else {
        populate();
      }
    }
  }

  public PagerAdapter getAdapter() {
    return mAdapter;
  }

  /**
   * Set the currently selected page. If the ViewPager has already been
   * through its first layout there will be a smooth animated transition
   * between the current item and the specified item.
   * 
   * @param item
   *            Item index to select
   */
  public void setCurrentItem(int item) {
    mPopulatePending = false;
    setCurrentItemInternal(item, !mFirstLayout, false);
  }

  /**
   * Set the currently selected page.
   * 
   * @param item
   *            Item index to select
   * @param smoothScroll
   *            True to smoothly scroll to the new item, false to transition
   *            immediately
   */
  public void setCurrentItem(int item, boolean smoothScroll) {
    mPopulatePending = false;
    setCurrentItemInternal(item, smoothScroll, false);
  }

  public int getCurrentItem() {
    return mCurItem;
  }

  void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
    setCurrentItemInternal(item, smoothScroll, always, 0);
  }

  void setCurrentItemInternal(int item, boolean smoothScroll, boolean always,
      int velocity) {
    if (mAdapter == null || mAdapter.getCount() <= 0) {
      setScrollingCacheEnabled(false);
      return;
    }
    if (!always && mCurItem == item && mItems.size() != 0) {
      setScrollingCacheEnabled(false);
      return;
    }
    if (item < 0) {
      item = 0;
    } else if (item >= mAdapter.getCount()) {
      item = mAdapter.getCount() - 1;
    }
    final int pageLimit = mOffscreenPageLimit;
    if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
      // We are doing a jump by more than one page. To avoid
      // glitches, we want to keep all current pages in the view
      // until the scroll ends.
      for (int i = 0; i < mItems.size(); i++) {
        mItems.get(i).scrolling = true;
      }
    }

    final boolean dispatchSelected = mCurItem != item;
    mCurItem = item;
    populate();
    final int destX = (getWidth() + mPageMargin) * item;
    if (smoothScroll) {
      smoothScrollTo(destX, 0, velocity);
      if (dispatchSelected && mOnPageChangeListener != null) {
        mOnPageChangeListener.onPageSelected(item);
      }
    } else {
      if (dispatchSelected && mOnPageChangeListener != null) {
        mOnPageChangeListener.onPageSelected(item);
      }
      completeScroll();
      scrollTo(destX, 0);
    }
  }

  public void setOnPageChangeListener(OnPageChangeListener listener) {
    mOnPageChangeListener = listener;
  }

  /**
   * Returns the number of pages that will be retained to either side of the
   * current page in the view hierarchy in an idle state. Defaults to 1.
   * 
   * @return How many pages will be kept offscreen on either side
   * @see #setOffscreenPageLimit(int)
   */
  public int getOffscreenPageLimit() {
    return mOffscreenPageLimit;
  }

  /**
   * Set the number of pages that should be retained to either side of the
   * current page in the view hierarchy in an idle state. Pages beyond this
   * limit will be recreated from the adapter when needed.
   * 
   * <p>
   * This is offered as an optimization. If you know in advance the number of
   * pages you will need to support or have lazy-loading mechanisms in place
   * on your pages, tweaking this setting can have benefits in perceived
   * smoothness of paging animations and interaction. If you have a small
   * number of pages (3-4) that you can keep active all at once, less time
   * will be spent in layout for newly created view subtrees as the user pages
   * back and forth.
   * </p>
   * 
   * <p>
   * You should keep this limit low, especially if your pages have complex
   * layouts. This setting defaults to 1.
   * </p>
   * 
   * @param limit
   *            How many pages will be kept offscreen in an idle state.
   */
  public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
      Log.w(TAG, "Requested offscreen page limit " + limit
          + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES);
      limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
      mOffscreenPageLimit = limit;
      populate();
    }
  }

  /**
   * Set the margin between pages.
   * 
   * @param marginPixels
   *            Distance between adjacent pages in pixels
   * @see #getPageMargin()
   * @see #setPageMarginDrawable(android.graphics.drawable.Drawable)
   * @see #setPageMarginDrawable(int)
   */
  public void setPageMargin(int marginPixels) {
    final int oldMargin = mPageMargin;
    mPageMargin = marginPixels;

    final int width = getWidth();
    recomputeScrollPosition(width, width, marginPixels, oldMargin);

    requestLayout();
  }

  /**
   * Return the margin between pages.
   * 
   * @return The size of the margin in pixels
   */
  public int getPageMargin() {
    return mPageMargin;
  }

  /**
   * Set a drawable that will be used to fill the margin between pages.
   * 
   * @param d
   *            Drawable to display between pages
   */
  public void setPageMarginDrawable(Drawable d) {
    mMarginDrawable = d;
    if (d != null)
      refreshDrawableState();
    setWillNotDraw(d == null);
    invalidate();
  }

  /**
   * Set a drawable that will be used to fill the margin between pages.
   * 
   * @param resId
   *            Resource ID of a drawable to display between pages
   */
  public void setPageMarginDrawable(int resId) {
    setPageMarginDrawable(getContext().getResources().getDrawable(resId));
  }

  @Override
  protected boolean verifyDrawable(Drawable who) {
    return super.verifyDrawable(who) || who == mMarginDrawable;
  }

  @Override
  protected void drawableStateChanged() {
    super.drawableStateChanged();
    final Drawable d = mMarginDrawable;
    if (d != null && d.isStateful()) {
      d.setState(getDrawableState());
    }
  }

  // We want the duration of the page snap animation to be influenced by the
  // distance that
  // the screen has to travel, however, we don't want this duration to be
  // effected in a
  // purely linear fashion. Instead, we use this method to moderate the effect
  // that the distance
  // of travel has on the overall snap duration.
  float distanceInfluenceForSnapDuration(float f) {
    f -= 0.5f; // center the values about 0.
    f *= 0.3f * Math.PI / 2.0f;
    return (float) Math.sin(f);
  }

  /**
   * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of
   * immediately.
   * 
   * @param x
   *            the number of pixels to scroll by on the X axis
   * @param y
   *            the number of pixels to scroll by on the Y axis
   */
  void smoothScrollTo(int x, int y) {
    smoothScrollTo(x, y, 0);
  }

  /**
   * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of
   * immediately.
   * 
   * @param x
   *            the number of pixels to scroll by on the X axis
   * @param y
   *            the number of pixels to scroll by on the Y axis
   * @param velocity
   *            the velocity associated with a fling, if applicable. (0
   *            otherwise)
   */
  void smoothScrollTo(int x, int y, int velocity) {
    if (getChildCount() == 0) {
      // Nothing to do.
      setScrollingCacheEnabled(false);
      return;
    }
    int sx = getScrollX();
    int sy = getScrollY();
    int dx = x - sx;
    int dy = y - sy;
    if (dx == 0 && dy == 0) {
      completeScroll();
      setScrollState(SCROLL_STATE_IDLE);
      return;
    }

    setScrollingCacheEnabled(true);
    mScrolling = true;
    setScrollState(SCROLL_STATE_SETTLING);

    final float pageDelta = (float) Math.abs(dx)
        / (getWidth() + mPageMargin);
    int duration = (int) (pageDelta * 100);

    velocity = Math.abs(velocity);
    if (velocity > 0) {
      duration += (duration / (velocity / mBaseLineFlingVelocity))
          * mFlingVelocityInfluence;
    } else {
      duration += 100;
    }
    duration = Math.min(duration, MAX_SETTLE_DURATION);

    mScroller.startScroll(sx, sy, dx, dy, duration);
    invalidate();
  }

  void addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);
    if (index < 0) {
      mItems.add(ii);
    } else {
      mItems.add(index, ii);
    }
  }

  void dataSetChanged() {
    // This method only gets called if our observer is attached, so mAdapter
    // is non-null.

    boolean needPopulate = mItems.size() < 3
        && mItems.size() < mAdapter.getCount();
    int newCurrItem = -1;

    for (int i = 0; i < mItems.size(); i++) {
      final ItemInfo ii = mItems.get(i);
      final int newPos = mAdapter.getItemPosition(ii.object);

      if (newPos == PagerAdapter.POSITION_UNCHANGED) {
        continue;
      }

      if (newPos == PagerAdapter.POSITION_NONE) {
        mItems.remove(i);
        i--;
        mAdapter.destroyItem(this, ii.position, ii.object);
        needPopulate = true;

        if (mCurItem == ii.position) {
          // Keep the current item in the valid range
          newCurrItem = Math.max(0,
              Math.min(mCurItem, mAdapter.getCount() - 1));
        }
        continue;
      }

      if (ii.position != newPos) {
        if (ii.position == mCurItem) {
          // Our current item changed position. Follow it.
          newCurrItem = newPos;
        }

        ii.position = newPos;
        needPopulate = true;
      }
    }

    Collections.sort(mItems, COMPARATOR);

    if (newCurrItem >= 0) {
      // TODO This currently causes a jump.
      setCurrentItemInternal(newCurrItem, false, true);
      needPopulate = true;
    }
    if (needPopulate) {
      populate();
      requestLayout();
    }
  }

  void populate() {
    if (mAdapter == null) {
      return;
    }

    // Bail now if we are waiting to populate. This is to hold off
    // on creating views from the time the user releases their finger to
    // fling to a new position until we have finished the scroll to
    // that position, avoiding glitches from happening at that point.
    if (mPopulatePending) {
      if (DEBUG)
        Log.i(TAG, "populate is pending, skipping for now...");
      return;
    }

    // Also, don't populate until we are attached to a window. This is to
    // avoid trying to populate before we have restored our view hierarchy
    // state and conflicting with what is restored.
    if (getWindowToken() == null) {
      return;
    }

    mAdapter.startUpdate(this);

    final int pageLimit = mOffscreenPageLimit;
    final int startPos = Math.max(0, mCurItem - pageLimit);
    final int N = mAdapter.getCount();
    final int endPos = Math.min(N - 1, mCurItem + pageLimit);

    if (DEBUG)
      Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos);

    // Add and remove pages in the existing list.
    int lastPos = -1;
    for (int i = 0; i < mItems.size(); i++) {
      ItemInfo ii = mItems.get(i);
      if ((ii.position < startPos || ii.position > endPos)
          && !ii.scrolling) {
        if (DEBUG)
          Log.i(TAG, "removing: " + ii.position + " @ " + i);
        mItems.remove(i);
        i--;
        mAdapter.destroyItem(this, ii.position, ii.object);
      } else if (lastPos < endPos && ii.position > startPos) {
        // The next item is outside of our range, but we have a gap
        // between it and the last item where we want to have a page
        // shown. Fill in the gap.
        lastPos++;
        if (lastPos < startPos) {
          lastPos = startPos;
        }
        while (lastPos <= endPos && lastPos < ii.position) {
          if (DEBUG)
            Log.i(TAG, "inserting: " + lastPos + " @ " + i);
          addNewItem(lastPos, i);
          lastPos++;
          i++;
        }
      }
      lastPos = ii.position;
    }

    // Add any new pages we need at the end.
    lastPos = mItems.size() > 0 ? mItems.get(mItems.size() - 1).position
        : -1;
    if (lastPos < endPos) {
      lastPos++;
      lastPos = lastPos > startPos ? lastPos : startPos;
      while (lastPos <= endPos) {
        if (DEBUG)
          Log.i(TAG, "appending: " + lastPos);
        addNewItem(lastPos, -1);
        lastPos++;
      }
    }

    if (DEBUG) {
      Log.i(TAG, "Current page list:");
      for (int i = 0; i < mItems.size(); i++) {
        Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
      }
    }

    ItemInfo curItem = null;
    for (int i = 0; i < mItems.size(); i++) {
      if (mItems.get(i).position == mCurItem) {
        curItem = mItems.get(i);
        break;
      }
    }
    mAdapter.setPrimaryItem(this, mCurItem,
        curItem != null ? curItem.object : null);

    mAdapter.finishUpdate(this);

    if (hasFocus()) {
      View currentFocused = findFocus();
      ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused)
          : null;
      if (ii == null || ii.position != mCurItem) {
        for (int i = 0; i < getChildCount(); i++) {
          View child = getChildAt(i);
          ii = infoForChild(child);
          if (ii != null && ii.position == mCurItem) {
            if (child.requestFocus(FOCUS_FORWARD)) {
              break;
            }
          }
        }
      }
    }
  }

  public static class SavedState extends BaseSavedState {
    int position;
    Parcelable adapterState;
    ClassLoader loader;

    public SavedState(Parcelable superState) {
      super(superState);
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeInt(position);
      out.writeParcelable(adapterState, flags);
    }

    @Override
    public String toString() {
      return "FragmentPager.SavedState{"
          + Integer.toHexString(System.identityHashCode(this))
          + " position=" + position + "}";
    }

    public static final Creator<SavedState> CREATOR = ParcelableCompat
        .newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
          @Override
          public SavedState createFromParcel(Parcel in,
              ClassLoader loader) {
            return new SavedState(in, loader);
          }

          @Override
          public SavedState[] newArray(int size) {
            return new SavedState[size];
          }
        });

    SavedState(Parcel in, ClassLoader loader) {
      super(in);
      if (loader == null) {
        loader = getClass().getClassLoader();
      }
      position = in.readInt();
      adapterState = in.readParcelable(loader);
      this.loader = loader;
    }
  }

  @Override
  public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.position = mCurItem;
    if (mAdapter != null) {
      ss.adapterState = mAdapter.saveState();
    }
    return ss;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
      super.onRestoreInstanceState(state);
      return;
    }

    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());

    if (mAdapter != null) {
      mAdapter.restoreState(ss.adapterState, ss.loader);
      setCurrentItemInternal(ss.position, false, true);
    } else {
      mRestoredCurItem = ss.position;
      mRestoredAdapterState = ss.adapterState;
      mRestoredClassLoader = ss.loader;
    }
  }

  @Override
  public void addView(View child, int index, LayoutParams params) {
    if (mInLayout) {
      addViewInLayout(child, index, params);
      child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
    } else {
      super.addView(child, index, params);
    }

    if (USE_CACHE) {
      if (child.getVisibility() != GONE) {
        child.setDrawingCacheEnabled(mScrollingCacheEnabled);
      } else {
        child.setDrawingCacheEnabled(false);
      }
    }
  }

  ItemInfo infoForChild(View child) {
    for (int i = 0; i < mItems.size(); i++) {
      ItemInfo ii = mItems.get(i);
      if (mAdapter.isViewFromObject(child, ii.object)) {
        return ii;
      }
    }
    return null;
  }

  ItemInfo infoForAnyChild(View child) {
    ViewParent parent;
    while ((parent = child.getParent()) != this) {
      if (parent == null || !(parent instanceof View)) {
        return null;
      }
      child = (View) parent;
    }
    return infoForChild(child);
  }

  @Override
  protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    mFirstLayout = true;
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // For simple implementation, or internal size is always 0.
    // We depend on the container to specify the layout size of
    // our view. We can't really know what it is since we will be
    // adding and removing different arbitrary views and do not
    // want the layout to change as this happens.
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
        getDefaultSize(0, heightMeasureSpec));

    // Children are just made to fill our space.
    mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth()
        - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
    mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
        getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
        MeasureSpec.EXACTLY);

    // Make sure we have created all fragments that we need to have shown.
    mInLayout = true;
    populate();
    mInLayout = false;

    // Make sure all children have been properly measured.
    final int size = getChildCount();
    for (int i = 0; i < size; ++i) {
      final View child = getChildAt(i);
      if (child.getVisibility() != GONE) {
        if (DEBUG)
          Log.v(TAG, "Measuring #" + i + " " + child + ": "
              + mChildWidthMeasureSpec);
        child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
      }
    }
  }

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    // Make sure scroll position is set correctly.
    if (w != oldw) {
      recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
    }
  }

  private void recomputeScrollPosition(int width, int oldWidth, int margin,
      int oldMargin) {
    final int widthWithMargin = width + margin;
    if (oldWidth > 0) {
      final int oldScrollPos = getScrollX();
      final int oldwwm = oldWidth + oldMargin;
      final int oldScrollItem = oldScrollPos / oldwwm;
      final float scrollOffset = (float) (oldScrollPos % oldwwm) / oldwwm;
      final int scrollPos = (int) ((oldScrollItem + scrollOffset) * widthWithMargin);
      scrollTo(scrollPos, getScrollY());
      if (!mScroller.isFinished()) {
        // We now return to your regularly scheduled scroll, already in
        // progress.
        final int newDuration = mScroller.getDuration()
            - mScroller.timePassed();
        mScroller.startScroll(scrollPos, 0, mCurItem * widthWithMargin,
            0, newDuration);
      }
    } else {
      int scrollPos = mCurItem * widthWithMargin;
      if (scrollPos != getScrollX()) {
        completeScroll();
        scrollTo(scrollPos, getScrollY());
      }
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mInLayout = true;
    populate();
    mInLayout = false;

    final int count = getChildCount();
    final int width = r - l;

    for (int i = 0; i < count; i++) {
      View child = getChildAt(i);
      ItemInfo ii;
      if (child.getVisibility() != GONE
          && (ii = infoForChild(child)) != null) {
        int loff = (width + mPageMargin) * ii.position;
        int childLeft = getPaddingLeft() + loff;
        int childTop = getPaddingTop();
        if (DEBUG)
          Log.v(TAG,
              "Positioning #" + i + " " + child + " f="
                  + ii.object + ":" + childLeft + ","
                  + childTop + " " + child.getMeasuredWidth()
                  + "x" + child.getMeasuredHeight());
        child.layout(childLeft, childTop,
            childLeft + child.getMeasuredWidth(),
            childTop + child.getMeasuredHeight());
      }
    }
    mFirstLayout = false;
  }

  @Override
  public void computeScroll() {
    if (DEBUG)
      Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
    if (!mScroller.isFinished()) {
      if (mScroller.computeScrollOffset()) {
        if (DEBUG)
          Log.i(TAG, "computeScroll: still scrolling");
        int oldX = getScrollX();
        int oldY = getScrollY();
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();

        if (oldX != x || oldY != y) {
          scrollTo(x, y);
        }

        if (mOnPageChangeListener != null) {
          final int widthWithMargin = getWidth() + mPageMargin;
          final int position = x / widthWithMargin;
          final int offsetPixels = x % widthWithMargin;
          final float offset = (float) offsetPixels / widthWithMargin;
          mOnPageChangeListener.onPageScrolled(position, offset,
              offsetPixels);
        }

        // Keep on drawing until the animation has finished.
        invalidate();
        return;
      }
    }

    // Done with scroll, clean up state.
    completeScroll();
  }

  private void completeScroll() {
    boolean needPopulate = mScrolling;
    if (needPopulate) {
      // Done with scroll, no longer want to cache view drawing.
      setScrollingCacheEnabled(false);
      mScroller.abortAnimation();
      int oldX = getScrollX();
      int oldY = getScrollY();
      int x = mScroller.getCurrX();
      int y = mScroller.getCurrY();
      if (oldX != x || oldY != y) {
        scrollTo(x, y);
      }
      setScrollState(SCROLL_STATE_IDLE);
    }
    mPopulatePending = false;
    mScrolling = false;
    for (int i = 0; i < mItems.size(); i++) {
      ItemInfo ii = mItems.get(i);
      if (ii.scrolling) {
        needPopulate = true;
        ii.scrolling = false;
      }
    }
    if (needPopulate) {
      populate();
    }
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    /*
     * This method JUST determines whether we want to intercept the motion.
     * If we return true, onMotionEvent will be called and we do the actual
     * scrolling there.
     */

    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    // Always take care of the touch gesture being complete.
    if (action == MotionEvent.ACTION_CANCEL
        || action == MotionEvent.ACTION_UP) {
      // Release the drag.
      if (DEBUG)
        Log.v(TAG, "Intercept done!");
      mIsBeingDragged = false;
      mIsUnableToDrag = false;
      mActivePointerId = INVALID_POINTER;
      return false;
    }

    // Nothing more to do here if we have decided whether or not we
    // are dragging.
    if (action != MotionEvent.ACTION_DOWN) {
      if (mIsBeingDragged) {
        if (DEBUG)
          Log.v(TAG, "Intercept returning true!");
        return true;
      }
      if (mIsUnableToDrag) {
        if (DEBUG)
          Log.v(TAG, "Intercept returning false!");
        return false;
      }
    }

    switch (action) {
    case MotionEvent.ACTION_MOVE: {
      /*
       * mIsBeingDragged == false, otherwise the shortcut would have
       * caught it. Check whether the user has moved far enough from his
       * original down touch.
       */

      /*
       * Locally do absolute value. mLastMotionY is set to the y value of
       * the down event.
       */
      final int activePointerId = mActivePointerId;
      if (activePointerId == INVALID_POINTER) {
        // If we don't have a valid id, the touch down wasn't on
        // content.
        break;
      }

      final int pointerIndex = MotionEventCompat.findPointerIndex(ev,
          activePointerId);
      final float x = MotionEventCompat.getX(ev, pointerIndex);
      final float dx = x - mLastMotionX;
      final float xDiff = Math.abs(dx);
      final float y = MotionEventCompat.getY(ev, pointerIndex);
      final float yDiff = Math.abs(y - mLastMotionY);
      final int scrollX = getScrollX();
      final boolean atEdge = (dx > 0 && scrollX == 0)
          || (dx < 0 && mAdapter != null && scrollX >= (mAdapter
              .getCount() - 1) * getWidth() - 1);
      if (DEBUG)
        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + ","
            + yDiff);

      if (canScroll(this, false, (int) dx, (int) x, (int) y)) {
        // Nested view has scrollable area under this point. Let it be
        // handled there.
        mInitialMotionX = mLastMotionX = x;
        mLastMotionY = y;
        return false;
      }
      if (xDiff > mTouchSlop && xDiff > yDiff) {
        if (DEBUG)
          Log.v(TAG, "Starting drag!");
        mIsBeingDragged = true;
        setScrollState(SCROLL_STATE_DRAGGING);
        mLastMotionX = x;
        setScrollingCacheEnabled(true);
      } else {
        if (yDiff > mTouchSlop) {
          // The finger has moved enough in the vertical
          // direction to be counted as a drag... abort
          // any attempt to drag horizontally, to work correctly
          // with children that have scrolling containers.
          if (DEBUG)
            Log.v(TAG, "Starting unable to drag!");
          mIsUnableToDrag = true;
        }
      }
      break;
    }

    case MotionEvent.ACTION_DOWN: {
      /*
       * Remember location of down touch. ACTION_DOWN always refers to
       * pointer index 0.
       */
      mLastMotionX = mInitialMotionX = ev.getX();
      mLastMotionY = ev.getY();
      mActivePointerId = MotionEventCompat.getPointerId(ev, 0);

      if (mScrollState == SCROLL_STATE_SETTLING) {
        // Let the user 'catch' the pager as it animates.
        mIsBeingDragged = true;
        mIsUnableToDrag = false;
        setScrollState(SCROLL_STATE_DRAGGING);
      } else {
        completeScroll();
        mIsBeingDragged = false;
        mIsUnableToDrag = false;
      }

      if (DEBUG)
        Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
            + " mIsBeingDragged=" + mIsBeingDragged
            + "mIsUnableToDrag=" + mIsUnableToDrag);
      break;
    }

    case MotionEventCompat.ACTION_POINTER_UP:
      onSecondaryPointerUp(ev);
      break;
    }

    /*
     * The only time we want to intercept motion events is if we are in the
     * drag mode.
     */
    return mIsBeingDragged;
  }

  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    if (mFakeDragging) {
      // A fake drag is in progress already, ignore this real one
      // but still eat the touch events.
      // (It is likely that the user is multi-touching the screen.)
      return true;
    }

    if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
      // Don't handle edge touches immediately -- they may actually belong
      // to one of our
      // descendants.
      return false;
    }

    if (mAdapter == null || mAdapter.getCount() == 0) {
      // Nothing to present or scroll; nothing to touch.
      return false;
    }

    if (mVelocityTracker == null) {
      mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    final int action = ev.getAction();
    boolean needsInvalidate = false;

    switch (action & MotionEventCompat.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN: {
      /*
       * If being flinged and user touches, stop the fling. isFinished
       * will be false if being flinged.
       */
      completeScroll();

      // Remember where the motion event started
      mLastMotionX = mInitialMotionX = ev.getX();
      mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
      break;
    }
    case MotionEvent.ACTION_MOVE:
      if (!mIsBeingDragged) {
        final int pointerIndex = MotionEventCompat.findPointerIndex(ev,
            mActivePointerId);
        final float x = MotionEventCompat.getX(ev, pointerIndex);
        final float xDiff = Math.abs(x - mLastMotionX);
        final float y = MotionEventCompat.getY(ev, pointerIndex);
        final float yDiff = Math.abs(y - mLastMotionY);
        if (DEBUG)
          Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff
              + "," + yDiff);
        if (xDiff > mTouchSlop && xDiff > yDiff) {
          if (DEBUG)
            Log.v(TAG, "Starting drag!");
          mIsBeingDragged = true;
          mLastMotionX = x;
          setScrollState(SCROLL_STATE_DRAGGING);
          setScrollingCacheEnabled(true);
        }
      }
      if (mIsBeingDragged) {
        // Scroll to follow the motion event
        final int activePointerIndex = MotionEventCompat
            .findPointerIndex(ev, mActivePointerId);
        final float x = MotionEventCompat.getX(ev, activePointerIndex);
        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;
        float oldScrollX = getScrollX();
        float scrollX = oldScrollX + deltaX;
        final int width = getWidth();
        final int widthWithMargin = width + mPageMargin;

        final int lastItemIndex = mAdapter.getCount() - 1;
        final float leftBound = Math.max(0, (mCurItem - 1)
            * widthWithMargin);
        final float rightBound = Math.min(mCurItem + 1, lastItemIndex)
            * widthWithMargin;
        if (scrollX < leftBound) {
          if (leftBound == 0) {
            float over = -scrollX;
            needsInvalidate = mLeftEdge.onPull(over / width);
          }
          scrollX = leftBound;
        } else if (scrollX > rightBound) {
          if (rightBound == lastItemIndex * widthWithMargin) {
            float over = scrollX - rightBound;
            needsInvalidate = mRightEdge.onPull(over / width);
          }
          scrollX = rightBound;
        }
        // Don't lose the rounded component
        mLastMotionX += scrollX - (int) scrollX;
        scrollTo((int) scrollX, getScrollY());
        if (mOnPageChangeListener != null) {
          final int position = (int) scrollX / widthWithMargin;
          final int positionOffsetPixels = (int) scrollX
              % widthWithMargin;
          final float positionOffset = (float) positionOffsetPixels
              / widthWithMargin;
          mOnPageChangeListener.onPageScrolled(position,
              positionOffset, positionOffsetPixels);
        }
      }
      break;
    case MotionEvent.ACTION_UP:
      if (mIsBeingDragged) {
        final VelocityTracker velocityTracker = mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
            velocityTracker, mActivePointerId);
        mPopulatePending = true;
        final int widthWithMargin = getWidth() + mPageMargin;
        final int scrollX = getScrollX();
        final int currentPage = scrollX / widthWithMargin;
        int nextPage = initialVelocity > 0 ? currentPage
            : currentPage + 1;
        setCurrentItemInternal(nextPage, true, true, initialVelocity);

        mActivePointerId = INVALID_POINTER;
        endDrag();
        needsInvalidate = mLeftEdge.onRelease()
            | mRightEdge.onRelease();
      }
      break;
    case MotionEvent.ACTION_CANCEL:
      if (mIsBeingDragged) {
        setCurrentItemInternal(mCurItem, true, true);
        mActivePointerId = INVALID_POINTER;
        endDrag();
        needsInvalidate = mLeftEdge.onRelease()
            | mRightEdge.onRelease();
      }
      break;
    case MotionEventCompat.ACTION_POINTER_DOWN: {
      final int index = MotionEventCompat.getActionIndex(ev);
      final float x = MotionEventCompat.getX(ev, index);
      mLastMotionX = x;
      mActivePointerId = MotionEventCompat.getPointerId(ev, index);
      break;
    }
    case MotionEventCompat.ACTION_POINTER_UP:
      onSecondaryPointerUp(ev);
      mLastMotionX = MotionEventCompat.getX(ev,
          MotionEventCompat.findPointerIndex(ev, mActivePointerId));
      break;
    }
    if (needsInvalidate) {
      invalidate();
    }
    return true;
  }

  @Override
  public void draw(Canvas canvas) {
    super.draw(canvas);
    boolean needsInvalidate = false;

    final int overScrollMode = ViewCompat.getOverScrollMode(this);
    if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
        || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS
            && mAdapter != null && mAdapter.getCount() > 1)) {
      if (!mLeftEdge.isFinished()) {
        final int restoreCount = canvas.save();
        final int height = getHeight() - getPaddingTop()
            - getPaddingBottom();

        canvas.rotate(270);
        canvas.translate(-height + getPaddingTop(), 0);
        mLeftEdge.setSize(height, getWidth());
        needsInvalidate |= mLeftEdge.draw(canvas);
        canvas.restoreToCount(restoreCount);
      }
      if (!mRightEdge.isFinished()) {
        final int restoreCount = canvas.save();
        final int width = getWidth();
        final int height = getHeight() - getPaddingTop()
            - getPaddingBottom();
        final int itemCount = mAdapter != null ? mAdapter.getCount()
            : 1;

        canvas.rotate(90);
        canvas.translate(-getPaddingTop(), -itemCount
            * (width + mPageMargin) + mPageMargin);
        mRightEdge.setSize(height, width);
        needsInvalidate |= mRightEdge.draw(canvas);
        canvas.restoreToCount(restoreCount);
      }
    } else {
      mLeftEdge.finish();
      mRightEdge.finish();
    }

    if (needsInvalidate) {
      // Keep animating
      invalidate();
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Draw the margin drawable if needed.
    if (mPageMargin > 0 && mMarginDrawable != null) {
      final int scrollX = getScrollX();
      final int width = getWidth();
      final int offset = scrollX % (width + mPageMargin);
      if (offset != 0) {
        // Pages fit completely when settled; we only need to draw when
        // in between
        final int left = scrollX - offset + width;
        mMarginDrawable.setBounds(left, 0, left + mPageMargin,
            getHeight());
        mMarginDrawable.draw(canvas);
      }
    }
  }

  /**
   * Start a fake drag of the pager.
   * 
   * <p>
   * A fake drag can be useful if you want to synchronize the motion of the
   * ViewPager with the touch scrolling of another view, while still letting
   * the ViewPager control the snapping motion and fling behavior. (e.g.
   * parallax-scrolling tabs.) Call {@link #fakeDragBy(float)} to simulate the
   * actual drag motion. Call {@link #endFakeDrag()} to complete the fake drag
   * and fling as necessary.
   * 
   * <p>
   * During a fake drag the ViewPager will ignore all touch events. If a real
   * drag is already in progress, this method will return false.
   * 
   * @return true if the fake drag began successfully, false if it could not
   *         be started.
   * 
   * @see #fakeDragBy(float)
   * @see #endFakeDrag()
   */
  public boolean beginFakeDrag() {
    if (mIsBeingDragged) {
      return false;
    }
    mFakeDragging = true;
    setScrollState(SCROLL_STATE_DRAGGING);
    mInitialMotionX = mLastMotionX = 0;
    if (mVelocityTracker == null) {
      mVelocityTracker = VelocityTracker.obtain();
    } else {
      mVelocityTracker.clear();
    }
    final long time = SystemClock.uptimeMillis();
    final MotionEvent ev = MotionEvent.obtain(time, time,
        MotionEvent.ACTION_DOWN, 0, 0, 0);
    mVelocityTracker.addMovement(ev);
    ev.recycle();
    mFakeDragBeginTime = time;
    return true;
  }

  /**
   * End a fake drag of the pager.
   * 
   * @see #beginFakeDrag()
   * @see #fakeDragBy(float)
   */
  public void endFakeDrag() {
    if (!mFakeDragging) {
      throw new IllegalStateException(
          "No fake drag in progress. Call beginFakeDrag first.");
    }

    final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
        velocityTracker, mActivePointerId);
    mPopulatePending = true;
    if ((Math.abs(initialVelocity) > mMinimumVelocity)
        || Math.abs(mInitialMotionX - mLastMotionX) >= (getWidth() / 3)) {
      if (mLastMotionX > mInitialMotionX) {
        setCurrentItemInternal(mCurItem - 1, true, true);
      } else {
        setCurrentItemInternal(mCurItem + 1, true, true);
      }
    } else {
      setCurrentItemInternal(mCurItem, true, true);
    }
    endDrag();

    mFakeDragging = false;
  }

  /**
   * Fake drag by an offset in pixels. You must have called
   * {@link #beginFakeDrag()} first.
   * 
   * @param xOffset
   *            Offset in pixels to drag by.
   * @see #beginFakeDrag()
   * @see #endFakeDrag()
   */
  public void fakeDragBy(float xOffset) {
    if (!mFakeDragging) {
      throw new IllegalStateException(
          "No fake drag in progress. Call beginFakeDrag first.");
    }

    mLastMotionX += xOffset;
    float scrollX = getScrollX() - xOffset;
    final int width = getWidth();
    final int widthWithMargin = width + mPageMargin;

    final float leftBound = Math.max(0, (mCurItem - 1) * widthWithMargin);
    final float rightBound = Math
        .min(mCurItem + 1, mAdapter.getCount() - 1) * widthWithMargin;
    if (scrollX < leftBound) {
      scrollX = leftBound;
    } else if (scrollX > rightBound) {
      scrollX = rightBound;
    }
    // Don't lose the rounded component
    mLastMotionX += scrollX - (int) scrollX;
    scrollTo((int) scrollX, getScrollY());
    if (mOnPageChangeListener != null) {
      final int position = (int) scrollX / widthWithMargin;
      final int positionOffsetPixels = (int) scrollX % widthWithMargin;
      final float positionOffset = (float) positionOffsetPixels
          / widthWithMargin;
      mOnPageChangeListener.onPageScrolled(position, positionOffset,
          positionOffsetPixels);
    }

    // Synthesize an event for the VelocityTracker.
    final long time = SystemClock.uptimeMillis();
    final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time,
        MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0);
    mVelocityTracker.addMovement(ev);
    ev.recycle();
  }

  /**
   * Returns true if a fake drag is in progress.
   * 
   * @return true if currently in a fake drag, false otherwise.
   * 
   * @see #beginFakeDrag()
   * @see #fakeDragBy(float)
   * @see #endFakeDrag()
   */
  public boolean isFakeDragging() {
    return mFakeDragging;
  }

  private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
    if (pointerId == mActivePointerId) {
      // This was our active pointer going up. Choose a new
      // active pointer and adjust accordingly.
      final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
      mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
      mActivePointerId = MotionEventCompat.getPointerId(ev,
          newPointerIndex);
      if (mVelocityTracker != null) {
        mVelocityTracker.clear();
      }
    }
  }

  private void endDrag() {
    mIsBeingDragged = false;
    mIsUnableToDrag = false;

    if (mVelocityTracker != null) {
      mVelocityTracker.recycle();
      mVelocityTracker = null;
    }
  }

  private void setScrollingCacheEnabled(boolean enabled) {
    if (mScrollingCacheEnabled != enabled) {
      mScrollingCacheEnabled = enabled;
      if (USE_CACHE) {
        final int size = getChildCount();
        for (int i = 0; i < size; ++i) {
          final View child = getChildAt(i);
          if (child.getVisibility() != GONE) {
            child.setDrawingCacheEnabled(enabled);
          }
        }
      }
    }
  }

  /**
   * Tests scrollability within child views of v given a delta of dx.
   * 
   * @param v
   *            View to test for horizontal scrollability
   * @param checkV
   *            Whether the view v passed should itself be checked for
   *            scrollability (true), or just its children (false).
   * @param dx
   *            Delta scrolled in pixels
   * @param x
   *            X coordinate of the active touch point
   * @param y
   *            Y coordinate of the active touch point
   * @return true if child views of v can be scrolled by delta of dx.
   */
  protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    if (v instanceof ViewGroup) {
      final ViewGroup group = (ViewGroup) v;
      final int scrollX = v.getScrollX();
      final int scrollY = v.getScrollY();
      final int count = group.getChildCount();
      // Count backwards - let topmost views consume scroll distance
      // first.
      for (int i = count - 1; i >= 0; i--) {
        // TODO: Add versioned support here for transformed views.
        // This will not work for transformed views in Honeycomb+
        final View child = group.getChildAt(i);
        if (x + scrollX >= child.getLeft()
            && x + scrollX < child.getRight()
            && y + scrollY >= child.getTop()
            && y + scrollY < child.getBottom()
            && canScroll(child, true, dx,
                x + scrollX - child.getLeft(), y + scrollY
                    - child.getTop())) {
          return true;
        }
      }
    }

    return checkV && ViewCompat.canScrollHorizontally(v, -dx);
  }

  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    // Let the focused view and/or our descendants get the key first
    return super.dispatchKeyEvent(event) || executeKeyEvent(event);
  }

  /**
   * You can call this function yourself to have the scroll view perform
   * scrolling from a key event, just as if the event had been dispatched to
   * it by the view hierarchy.
   * 
   * @param event
   *            The key event to execute.
   * @return Return true if the event was handled, else false.
   */
  public boolean executeKeyEvent(KeyEvent event) {
    boolean handled = false;
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
      switch (event.getKeyCode()) {
      case KeyEvent.KEYCODE_DPAD_LEFT:
        handled = arrowScroll(FOCUS_LEFT);
        break;
      case KeyEvent.KEYCODE_DPAD_RIGHT:
        handled = arrowScroll(FOCUS_RIGHT);
        break;
      case KeyEvent.KEYCODE_TAB:
        if (KeyEventCompat.hasNoModifiers(event)) {
          handled = arrowScroll(FOCUS_FORWARD);
        } else if (KeyEventCompat.hasModifiers(event,
            KeyEvent.META_SHIFT_ON)) {
          handled = arrowScroll(FOCUS_BACKWARD);
        }
        break;
      }
    }
    return handled;
  }

  public boolean arrowScroll(int direction) {
    View currentFocused = findFocus();
    if (currentFocused == this)
      currentFocused = null;

    boolean handled = false;

    View nextFocused = FocusFinder.getInstance().findNextFocus(this,
        currentFocused, direction);
    if (nextFocused != null && nextFocused != currentFocused) {
      if (direction == View.FOCUS_LEFT) {
        // If there is nothing to the left, or this is causing us to
        // jump to the right, then what we really want to do is page
        // left.
        if (currentFocused != null
            && nextFocused.getLeft() >= currentFocused.getLeft()) {
          handled = pageLeft();
        } else {
          handled = nextFocused.requestFocus();
        }
      } else if (direction == View.FOCUS_RIGHT) {
        // If there is nothing to the right, or this is causing us to
        // jump to the left, then what we really want to do is page
        // right.
        if (currentFocused != null
            && nextFocused.getLeft() <= currentFocused.getLeft()) {
          handled = pageRight();
        } else {
          handled = nextFocused.requestFocus();
        }
      }
    } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
      // Trying to move left and nothing there; try to page.
      handled = pageLeft();
    } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
      // Trying to move right and nothing there; try to page.
      handled = pageRight();
    }
    if (handled) {
      playSoundEffect(SoundEffectConstants
          .getContantForFocusDirection(direction));
    }
    return handled;
  }

  boolean pageLeft() {
    if (mCurItem > 0) {
      setCurrentItem(mCurItem - 1, true);
      return true;
    }
    return false;
  }

  boolean pageRight() {
    if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
      setCurrentItem(mCurItem + 1, true);
      return true;
    }
    return false;
  }

  /**
   * We only want the current page that is being shown to be focusable.
   */
  @Override
  public void addFocusables(ArrayList<View> views, int direction,
      int focusableMode) {
    final int focusableCount = views.size();

    final int descendantFocusability = getDescendantFocusability();

    if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
      for (int i = 0; i < getChildCount(); i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() == VISIBLE) {
          ItemInfo ii = infoForChild(child);
          if (ii != null && ii.position == mCurItem) {
            child.addFocusables(views, direction, focusableMode);
          }
        }
      }
    }

    // we add ourselves (if focusable) in all cases except for when we are
    // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.
    // this is
    // to avoid the focus search finding layouts when a more precise search
    // among the focusable children would be more interesting.
    if (descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
    // No focusable descendants
        (focusableCount == views.size())) {
      // Note that we can't call the superclass here, because it will
      // add all views in. So we need to do the same thing View does.
      if (!isFocusable()) {
        return;
      }
      if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
          && isInTouchMode() && !isFocusableInTouchMode()) {
        return;
      }
      if (views != null) {
        views.add(this);
      }
    }
  }

  /**
   * We only want the current page that is being shown to be touchable.
   */
  @Override
  public void addTouchables(ArrayList<View> views) {
    // Note that we don't call super.addTouchables(), which means that
    // we don't call View.addTouchables(). This is okay because a ViewPager
    // is itself not touchable.
    for (int i = 0; i < getChildCount(); i++) {
      final View child = getChildAt(i);
      if (child.getVisibility() == VISIBLE) {
        ItemInfo ii = infoForChild(child);
        if (ii != null && ii.position == mCurItem) {
          child.addTouchables(views);
        }
      }
    }
  }

  /**
   * We only want the current page that is being shown to be focusable.
   */
  @Override
  protected boolean onRequestFocusInDescendants(int direction,
      Rect previouslyFocusedRect) {
    int index;
    int increment;
    int end;
    int count = getChildCount();
    if ((direction & FOCUS_FORWARD) != 0) {
      index = 0;
      increment = 1;
      end = count;
    } else {
      index = count - 1;
      increment = -1;
      end = -1;
    }
    for (int i = index; i != end; i += increment) {
      View child = getChildAt(i);
      if (child.getVisibility() == VISIBLE) {
        ItemInfo ii = infoForChild(child);
        if (ii != null && ii.position == mCurItem) {
          if (child.requestFocus(direction, previouslyFocusedRect)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  @Override
  public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    // ViewPagers should only report accessibility info for the current
    // page,
    // otherwise things get very confusing.

    // TODO: Should this note something about the paging container?

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
      final View child = getChildAt(i);
      if (child.getVisibility() == VISIBLE) {
        final ItemInfo ii = infoForChild(child);
        if (ii != null && ii.position == mCurItem
            && child.dispatchPopulateAccessibilityEvent(event)) {
          return true;
        }
      }
    }

    return false;
  }

  private class PagerObserver extends DataSetObserver {

    @Override
    public void onChanged() {
      dataSetChanged();
    }

    @Override
    public void onInvalidated() {
      dataSetChanged();
    }
  }
}